package com.loadburn.heron.utils.generics;

import java.io.Serializable;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class GenericsUtils {
    private static final Type UNBOUND_WILDCARD = new WildcardTypeImpl(new Type[]{Object.class},
            new Type[]{});

    /**
     * Returns the erasure of the given type.
     */
    public static Class<?> erase(Type type) {
        if (type instanceof Class<?>) {
            return (Class<?>) type;
        } else if (type instanceof ParameterizedType) {
            return (Class<?>) ((ParameterizedType) type).getRawType();
        } else if (type instanceof TypeVariable<?>) {
            TypeVariable<?> tv = (TypeVariable<?>) type;
            if (tv.getBounds().length == 0)
                return Object.class;
            else
                return erase(tv.getBounds()[0]);
        } else if (type instanceof GenericArrayType) {
            GenericArrayType aType = (GenericArrayType) type;
            return GenericArrayTypeImpl.createArrayType(erase(aType.getGenericComponentType()));
        } else {
            throw new RuntimeException("not supported: " + type.getClass());
        }
    }

    /**
     * Maps type parameters in a type to their values.
     *
     * @param toMapType     Type possibly containing type arguments
     * @param typeAndParams must be either ParameterizedType, or (in case there are no
     *                      type arguments, or it's a raw type) Class
     * @return toMapType, but with type parameters from typeAndParams replaced.
     */
    private static Type mapTypeParameters(Type toMapType, Type typeAndParams) {
        if (isMissingTypeParameters(typeAndParams)) {
            return erase(toMapType);
        } else {
            VariableMap variableMap = new VariableMap();
            Type handlingTypeAndParams = typeAndParams;
            while (handlingTypeAndParams instanceof ParameterizedType) {
                ParameterizedType pType = (ParameterizedType) handlingTypeAndParams;
                Class<?> clazz = (Class<?>) pType.getRawType(); // getRawType
                // should always
                // be Class
                variableMap.addAll(clazz.getTypeParameters(), pType.getActualTypeArguments());
                handlingTypeAndParams = pType.getOwnerType();
            }
            return variableMap.map(toMapType);
        }
    }

    /**
     * Checks if the given type is a class that is supposed to have type
     * parameters, but doesn't. In other words, if it's a really raw type.
     */
    private static boolean isMissingTypeParameters(Type type) {
        if (type instanceof Class<?>) {
            for (Class<?> clazz = (Class<?>) type; clazz != null; clazz = clazz.getEnclosingClass()) {
                if (clazz.getTypeParameters().length != 0)
                    return true;
            }
            return false;
        } else if (type instanceof ParameterizedType) {
            return false;
        } else {
            throw new AssertionError("Unexpected type " + type.getClass());
        }
    }

    /**
     *
     * @param clazz
     * @return
     */
    public static Type addWildcardParameters(Class<?> clazz) {
        if (clazz.isArray()) {
            return GenericArrayTypeImpl.createArrayType(addWildcardParameters(clazz
                    .getComponentType()));
        } else if (isMissingTypeParameters(clazz)) {
            TypeVariable<?>[] vars = clazz.getTypeParameters();
            Type[] arguments = new Type[vars.length];
            Arrays.fill(arguments, UNBOUND_WILDCARD);
            Type owner = clazz.getDeclaringClass() == null ? null : addWildcardParameters(clazz
                    .getDeclaringClass());
            return new ParameterizedTypeImpl(clazz, arguments, owner);
        } else {
            return clazz;
        }
    }

    public static Type getExactSuperType(Type type, Class<?> searchClass) {
        if (type instanceof ParameterizedType || type instanceof Class<?>
                || type instanceof GenericArrayType) {
            Class<?> clazz = erase(type);

            if (searchClass == clazz) {
                return type;
            }

            if (!searchClass.isAssignableFrom(clazz))
                return null;
        }

        for (Type superType : getExactDirectSuperTypes(type)) {
            Type result = getExactSuperType(superType, searchClass);
            if (result != null)
                return result;
        }

        return null;
    }

    @SuppressWarnings("SuspiciousMethodCalls")
    public static Type getTypeParameter(Type type, TypeVariable<? extends Class<?>> variable) {
        Class<?> clazz = variable.getGenericDeclaration();
        Type superType = getExactSuperType(type, clazz);
        if (superType instanceof ParameterizedType) {
            int index = Arrays.asList(clazz.getTypeParameters()).indexOf(variable);
            return ((ParameterizedType) superType).getActualTypeArguments()[index];
        } else {
            return null;
        }
    }

    @SuppressWarnings("ConstantConditions")
    public static boolean isSuperType(Type superType, Type subType) {
        if (superType instanceof ParameterizedType || superType instanceof Class<?>
                || superType instanceof GenericArrayType) {
            Class<?> superClass = erase(superType);
            Type mappedSubType = getExactSuperType(capture(subType), superClass);
            if (mappedSubType == null) {
                return false;
            } else if (superType instanceof Class<?>) {
                return true;
            } else if (mappedSubType instanceof Class<?>) {
                return true;
            } else if (mappedSubType instanceof GenericArrayType) {
                Type superComponentType = getArrayComponentType(superType);
                assert superComponentType != null;
                Type mappedSubComponentType = getArrayComponentType(mappedSubType);
                assert mappedSubComponentType != null;
                return isSuperType(superComponentType, mappedSubComponentType);
            } else {
                assert mappedSubType instanceof ParameterizedType;
                ParameterizedType pMappedSubType = (ParameterizedType) mappedSubType;
                assert pMappedSubType.getRawType() == superClass;
                ParameterizedType pSuperType = (ParameterizedType) superType;

                Type[] superTypeArgs = pSuperType.getActualTypeArguments();
                Type[] subTypeArgs = pMappedSubType.getActualTypeArguments();
                assert superTypeArgs.length == subTypeArgs.length;
                for (int i = 0; i < superTypeArgs.length; i++) {
                    if (!contains(superTypeArgs[i], subTypeArgs[i])) {
                        return false;
                    }
                }
                // params of the class itself match, so if the owner types are
                // supertypes too, it's a supertype.
                return pSuperType.getOwnerType() == null
                        || isSuperType(pSuperType.getOwnerType(), pMappedSubType.getOwnerType());
            }
        } else if (superType instanceof CaptureType) {
            if (superType.equals(subType))
                return true;
            for (Type lowerBound : ((CaptureType) superType).getLowerBounds()) {
                if (isSuperType(lowerBound, subType)) {
                    return true;
                }
            }
            return false;
        } else if (superType instanceof GenericArrayType) {
            return isArraySupertype(superType, subType);
        } else {
            throw new RuntimeException("not implemented: " + superType.getClass());
        }
    }

    private static boolean isArraySupertype(Type arraySuperType, Type subType) {
        Type superTypeComponent = getArrayComponentType(arraySuperType);
        assert superTypeComponent != null;
        Type subTypeComponent = getArrayComponentType(subType);
        if (subTypeComponent == null) {
            return false;
        } else {
            return isSuperType(superTypeComponent, subTypeComponent);
        }
    }

    public static Type getArrayComponentType(Type type) {
        if (type instanceof Class<?>) {
            Class<?> clazz = (Class<?>) type;
            return clazz.getComponentType();
        } else if (type instanceof GenericArrayType) {
            GenericArrayType aType = (GenericArrayType) type;
            return aType.getGenericComponentType();
        } else {
            return null;
        }
    }

    private static boolean contains(Type containingType, Type containedType) {
        if (containingType instanceof WildcardType) {
            WildcardType wContainingType = (WildcardType) containingType;
            for (Type upperBound : wContainingType.getUpperBounds()) {
                if (!isSuperType(upperBound, containedType)) {
                    return false;
                }
            }
            for (Type lowerBound : wContainingType.getLowerBounds()) {
                if (!isSuperType(containedType, lowerBound)) {
                    return false;
                }
            }
            return true;
        } else {
            return containingType.equals(containedType);
        }
    }

    public static Type[] getExactDirectSuperTypes(Type type) {
        if (type instanceof ParameterizedType || type instanceof Class<?>) {
            Class<?> clazz;
            if (type instanceof ParameterizedType) {
                clazz = (Class<?>) ((ParameterizedType) type).getRawType();
            } else {
                clazz = (Class<?>) type;
                if (clazz.isArray())
                    return getArrayExactDirectSuperTypes(clazz);
            }

            Type[] superInterfaces = clazz.getGenericInterfaces();
            Type superClass = clazz.getGenericSuperclass();
            Type[] result;
            int resultIndex;
            if (superClass == null) {
                result = new Type[superInterfaces.length];
                resultIndex = 0;
            } else {
                result = new Type[superInterfaces.length + 1];
                resultIndex = 1;
                result[0] = mapTypeParameters(superClass, type);
            }
            for (Type superInterface : superInterfaces) {
                result[resultIndex++] = mapTypeParameters(superInterface, type);
            }

            return result;
        } else if (type instanceof TypeVariable<?>) {
            TypeVariable<?> tv = (TypeVariable<?>) type;
            return tv.getBounds();
        } else if (type instanceof WildcardType) {
            return ((WildcardType) type).getUpperBounds();
        } else if (type instanceof CaptureType) {
            return ((CaptureType) type).getUpperBounds();
        } else if (type instanceof GenericArrayType) {
            return getArrayExactDirectSuperTypes(type);
        } else {
            throw new RuntimeException("not implemented type: " + type);
        }
    }

    @SuppressWarnings("UnusedAssignment")
    private static Type[] getArrayExactDirectSuperTypes(Type arrayType) {

        Type typeComponent = getArrayComponentType(arrayType);

        Type[] result;
        int resultIndex;
        if (typeComponent instanceof Class<?> && ((Class<?>) typeComponent).isPrimitive()) {
            resultIndex = 0;
            result = new Type[3];
        } else {
            Type[] componentSupertypes = getExactDirectSuperTypes(typeComponent);
            result = new Type[componentSupertypes.length + 3];
            for (resultIndex = 0; resultIndex < componentSupertypes.length; resultIndex++) {
                result[resultIndex] = GenericArrayTypeImpl
                        .createArrayType(componentSupertypes[resultIndex]);
            }
        }
        result[resultIndex++] = Object.class;
        result[resultIndex++] = Cloneable.class;
        result[resultIndex++] = Serializable.class;
        return result;
    }

    /**
     *
     * @param m
     * @param type
     * @return
     */
    public static Type getExactReturnType(Method m, Type type) {
        Type returnType = m.getGenericReturnType();
        Type exactDeclaringType = getExactSuperType(capture(type), m.getDeclaringClass());
        return mapTypeParameters(returnType, exactDeclaringType);
    }

    /**
     *
     * @param f
     * @param type
     * @return
     */
    public static Type getExactFieldType(Field f, Type type) {
        Type returnType = f.getGenericType();
        Type exactDeclaringType = getExactSuperType(capture(type), f.getDeclaringClass());
        return mapTypeParameters(returnType, exactDeclaringType);
    }

    /**
     * Applies capture converter to the given type.
     */
    public static Type capture(Type type) {
        VariableMap variableMap = new VariableMap();
        List<CaptureTypeImpl> toInit = new ArrayList<CaptureTypeImpl>();
        if (type instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) type;
            Class<?> clazz = (Class<?>) pType.getRawType();
            Type[] arguments = pType.getActualTypeArguments();
            TypeVariable<?>[] vars = clazz.getTypeParameters();
            Type[] capturedArguments = new Type[arguments.length];
            assert arguments.length == vars.length;
            for (int i = 0; i < arguments.length; i++) {
                Type argument = arguments[i];
                if (argument instanceof WildcardType) {
                    CaptureTypeImpl captured = new CaptureTypeImpl((WildcardType) argument, vars[i]);
                    argument = captured;
                    toInit.add(captured);
                }
                capturedArguments[i] = argument;
                variableMap.add(vars[i], argument);
            }
            for (CaptureTypeImpl captured : toInit) {
                captured.init(variableMap);
            }
            Type ownerType = (pType.getOwnerType() == null) ? null : capture(pType.getOwnerType());
            return new ParameterizedTypeImpl(clazz, capturedArguments, ownerType);
        } else {
            return type;
        }
    }

    /**
     * Returns the display name of a Type.
     */
    public static String getTypeName(Type type) {
        if (type instanceof Class<?>) {
            Class<?> clazz = (Class<?>) type;
            return clazz.isArray() ? (getTypeName(clazz.getComponentType()) + "[]") : clazz.getName();
        } else {
            return type.toString();
        }
    }
}
